<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SVG Node with Resizing and Property Editing</title>
  <style>
    #canvas {
      border: 1px solid #ccc;
      background-color: #f9f9f9;
    }
    .node {
      cursor: move;
    }
    .connector {
      cursor: pointer;
    }
    .resize-handle {
      cursor: nwse-resize;
      fill: #fff;
      stroke: #000;
      stroke-width: 1px;
    }
    .selected {
      stroke: red;
    }
    .hidden {
      display: none;
    }
    #property-editor {
      position: fixed;
      right: 0px;
      top:0px;
      border: 1px solid #ccc;
      background: #fff;
      padding: 10px;
      display: none;
    }
  </style>
</head>
<body>
  <button id="addNode">Add Node</button>
  <svg id="canvas" width="800" height="600"></svg>
  <div id="property-editor">
    <h3>Node Properties</h3>
    <label for="node-data">Data (JSON):</label>
    <textarea id="node-data" rows="10" cols="30"></textarea>
    <button id="save-properties">Save</button>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const svgNS = 'http://www.w3.org/2000/svg';
      const canvas = document.getElementById('canvas');
      const addNodeButton = document.getElementById('addNode');
      const propertyEditor = document.getElementById('property-editor');
      const nodeDataTextArea = document.getElementById('node-data');
      const savePropertiesButton = document.getElementById('save-properties');

      const gridSize = 20; // 定义网格大小
      let nodes = [];
      let links = [];
      let selectedLink = null;
      let selectedNode = null;
      let isResizing = false;
      let currentHandle = null;

      // 画网格线
      function drawGrid() {
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;

        for (let x = gridSize; x < width; x += gridSize) {
          const line = document.createElementNS(svgNS, 'line');
          line.setAttribute('x1', x);
          line.setAttribute('y1', 0);
          line.setAttribute('x2', x);
          line.setAttribute('y2', height);
          line.setAttribute('stroke', '#e0e0e0');
          line.setAttribute('stroke-width', 1);
          canvas.appendChild(line);
        }

        for (let y = gridSize; y < height; y += gridSize) {
          const line = document.createElementNS(svgNS, 'line');
          line.setAttribute('x1', 0);
          line.setAttribute('y1', y);
          line.setAttribute('x2', width);
          line.setAttribute('y2', y);
          line.setAttribute('stroke', '#e0e0e0');
          line.setAttribute('stroke-width', 1);
          canvas.appendChild(line);
        }
      }

      drawGrid();

      addNodeButton.addEventListener('click', addNode);

      function addNode() {
        const newNode = createNode();
        canvas.appendChild(newNode);
        nodes.push(newNode);
        makeDraggable(newNode);
        makeResizable(newNode);
      }

      function createNode() {
        const group = document.createElementNS(svgNS, 'g');
        group.setAttribute('data-id', `node-${nodes.length + 1}`);
        group.setAttribute('data-properties', JSON.stringify({name: `Node ${nodes.length + 1}`}));

        const rect = document.createElementNS(svgNS, 'rect');
        rect.setAttribute('x', 50);
        rect.setAttribute('y', 50);
        rect.setAttribute('width', 100);
        rect.setAttribute('height', 50);
        rect.setAttribute('fill', '#007BFF');
        rect.setAttribute('stroke', '#000');
        rect.classList.add('draggable');

        const connectors = ['top', 'bottom', 'left', 'right'].map(position => createConnector(position));
        // 'top', 'bottom', 'left', 'right'
        const resizeHandles = ['top-left', 'top-right', 'bottom-left', 'bottom-right'].map(position => createResizeHandle(position));

        connectors.forEach(connector => group.appendChild(connector));
        resizeHandles.forEach(handle => group.appendChild(handle));
        group.appendChild(rect);
        group.classList.add('node');
        group.setAttribute('transform', 'translate(0, 0)');

        group.addEventListener('click', (e) => {
          if (e.target.tagName === 'rect') {
            showPropertyEditor(group);
          }
        });

        return group;
      }

      function createConnector(position) {
        const circle = document.createElementNS(svgNS, 'circle');
        circle.setAttribute('r', 5);
        circle.setAttribute('fill', '#FFF');
        circle.setAttribute('stroke', '#000');
        circle.setAttribute('stroke-width', 2);
        circle.classList.add('connector');
        circle.dataset.position = position;

        switch (position) {
          case 'top':
            circle.setAttribute('cx', 100);
            circle.setAttribute('cy', 45);
            break;
          case 'bottom':
            circle.setAttribute('cx', 100);
            circle.setAttribute('cy', 105);
            break;
          case 'left':
            circle.setAttribute('cx', 45);
            circle.setAttribute('cy', 75);
            break;
          case 'right':
            circle.setAttribute('cx', 155);
            circle.setAttribute('cy', 75);
            break;
        }

        return circle;
      }

      function createResizeHandle(position) {
        const rect = document.createElementNS(svgNS, 'rect');
        rect.setAttribute('width', 10);
        rect.setAttribute('height', 10);
        rect.setAttribute('fill', '#FFF');
        rect.setAttribute('stroke', '#000');
        rect.classList.add('resize-handle');
        rect.dataset.position = position;

        switch (position) {
          case 'top-left':
            rect.setAttribute('x', 40);
            rect.setAttribute('y', 40);
            break;
          case 'top-right':
            rect.setAttribute('x', 150);
            rect.setAttribute('y', 40);
            break;
          case 'bottom-left':
            rect.setAttribute('x', 40);
            rect.setAttribute('y', 100);
            break;
          case 'bottom-right':
            rect.setAttribute('x', 150);
            rect.setAttribute('y', 100);
            break;
          case 'top':
            rect.setAttribute('x', 95);
            rect.setAttribute('y', 40);
            break;
          case 'bottom':
            rect.setAttribute('x', 95);
            rect.setAttribute('y', 100);
            break;
          case 'left':
            rect.setAttribute('x', 40);
            rect.setAttribute('y', 70);
            break;
          case 'right':
            rect.setAttribute('x', 150);
            rect.setAttribute('y', 70);
            break;
        }

        return rect;
      }

      function makeDraggable(node) {
        let offsetX, offsetY;
        let isDragging = false;
        const rect = node.querySelector('.draggable');

        rect.addEventListener('mousedown', (e) => {
          e.preventDefault();
          const transform = node.getAttribute('transform');
          const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
          const startX = translate ? parseFloat(translate[1]) : 0;
          const startY = translate ? parseFloat(translate[2]) : 0;
          offsetX = e.clientX - startX;
          offsetY = e.clientY - startY;
          isDragging = true;
          document.addEventListener('mousemove', onMouseMove);
          document.addEventListener('mouseup', onMouseUp);
        });

        function onMouseMove(e) {
          if (!isDragging) return;
          const newX = e.clientX - offsetX;
          const newY = e.clientY - offsetY;

          // 对齐到最近的网格点
          const snappedX = Math.round(newX / gridSize) * gridSize;
          const snappedY = Math.round(newY / gridSize) * gridSize;

          if (!isNaN(snappedX) && !isNaN(snappedY)) {
            node.setAttribute('transform', `translate(${snappedX}, ${snappedY})`);
            requestAnimationFrame(updateLinks);
          }
        }

        function onMouseUp() {
          isDragging = false;
          document.removeEventListener('mousemove', onMouseMove);
          document.removeEventListener('mouseup', onMouseUp);
        }
      }

      function makeResizable(node) {
        const handles = node.querySelectorAll('.resize-handle');

        handles.forEach(handle => {
          handle.addEventListener('mousedown', (e) => {
            e.preventDefault();
            isResizing = true;
            currentHandle = handle;
            document.addEventListener('mousemove', onResizeMove);
            document.addEventListener('mouseup', onResizeUp);
          });
        });

        function onResizeMove(e) {
          if (!isResizing || !currentHandle) return;

          const transform = node.getAttribute('transform');
          const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
          const startX = translate ? parseFloat(translate[1]) : 0;
          const startY = translate ? parseFloat(translate[2]) : 0;
          const rect = node.querySelector('rect');
          const x = parseFloat(rect.getAttribute('x'));
          const y = parseFloat(rect.getAttribute('y'));
          const width = parseFloat(rect.getAttribute('width'));
          const height = parseFloat(rect.getAttribute('height'));
          let newWidth = width;
          let newHeight = height;
          let newX = startX;
          let newY = startY;

          switch (currentHandle.dataset.position) {
            case 'top-left':
              newWidth = width + (x - (e.clientX - startX));
              newHeight = height + (y - (e.clientY - startY));
              newX = e.clientX;
              newY = e.clientY;
              break;
            case 'top-right':
              newWidth = e.clientX - x;
              newHeight = height + (y - (e.clientY - startY));
              newY = e.clientY;
              break;
            case 'bottom-left':
              newWidth = width + (x - (e.clientX - startX));
              newHeight = e.clientY - y;
              newX = e.clientX;
              break;
            case 'bottom-right':
              newWidth = e.clientX - x;
              newHeight = e.clientY - y;
              break;
            case 'top':
              newHeight = height + (y - (e.clientY - startY));
              newY = e.clientY;
              break;
            case 'bottom':
              newHeight = e.clientY - y;
              break;
            case 'left':
              newWidth = width + (x - (e.clientX - startX));
              newX = e.clientX;
              break;
            case 'right':
              newWidth = e.clientX - x;
              break;
          }

          rect.setAttribute('width', newWidth);
          rect.setAttribute('height', newHeight);
          node.setAttribute('transform', `translate(${newX}, ${newY})`);
          updateHandles(node);
          updateConnectors(node);
          updateLinks();
        }

        function onResizeUp() {
          isResizing = false;
          currentHandle = null;
          document.removeEventListener('mousemove', onResizeMove);
          document.removeEventListener('mouseup', onResizeUp);
        }
      }

      function updateHandles(node) {
        const rect = node.querySelector('rect');
        const x = parseFloat(rect.getAttribute('x'));
        const y = parseFloat(rect.getAttribute('y'));
        const width = parseFloat(rect.getAttribute('width'));
        const height = parseFloat(rect.getAttribute('height'));
        const handles = node.querySelectorAll('.resize-handle');

        handles.forEach(handle => {
          switch (handle.dataset.position) {
            case 'top-left':
              handle.setAttribute('x', x - 10);
              handle.setAttribute('y', y - 10);
              break;
            case 'top-right':
              handle.setAttribute('x', x + width);
              handle.setAttribute('y', y - 10);
              break;
            case 'bottom-left':
              handle.setAttribute('x', x - 10);
              handle.setAttribute('y', y + height);
              break;
            case 'bottom-right':
              handle.setAttribute('x', x + width);
              handle.setAttribute('y', y + height);
              break;
            case 'top':
              handle.setAttribute('x', x + width / 2 - 5);
              handle.setAttribute('y', y - 10);
              break;
            case 'bottom':
              handle.setAttribute('x', x + width / 2 - 5);
              handle.setAttribute('y', y + height);
              break;
            case 'left':
              handle.setAttribute('x', x - 10);
              handle.setAttribute('y', y + height / 2 - 5);
              break;
            case 'right':
              handle.setAttribute('x', x + width);
              handle.setAttribute('y', y + height / 2 - 5);
              break;
          }
        });
      }

      function updateConnectors(node) {
        const rect = node.querySelector('rect');
        const x = parseFloat(rect.getAttribute('x'));
        const y = parseFloat(rect.getAttribute('y'));
        const width = parseFloat(rect.getAttribute('width'));
        const height = parseFloat(rect.getAttribute('height'));
        const connectors = node.querySelectorAll('.connector');

        connectors.forEach(connector => {
          switch (connector.dataset.position) {
            case 'top':
              connector.setAttribute('cx', x + width / 2);
              connector.setAttribute('cy', y - 5);
              break;
            case 'bottom':
              connector.setAttribute('cx', x + width / 2);
              connector.setAttribute('cy', y + height + 5);
              break;
            case 'left':
              connector.setAttribute('cx', x - 5);
              connector.setAttribute('cy', y + height / 2);
              break;
            case 'right':
              connector.setAttribute('cx', x + width + 5);
              connector.setAttribute('cy', y + height / 2);
              break;
          }
        });
      }

      function showPropertyEditor(node) {
        const rect = node.querySelector('rect');
        const transform = node.getAttribute('transform');
        const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
        const x = translate ? parseFloat(translate[1]) : 0;
        const y = translate ? parseFloat(translate[2]) : 0;

        // propertyEditor.style.left = `${x + parseFloat(rect.getAttribute('x')) + parseFloat(rect.getAttribute('width')) + 10}px`;
        // propertyEditor.style.top = `${y + parseFloat(rect.getAttribute('y'))}px`;
        propertyEditor.style.display = 'block';

        selectedNode = node;
        const properties = JSON.parse(node.getAttribute('data-properties'));
        nodeDataTextArea.value = JSON.stringify(properties, null, 2);
      }

      savePropertiesButton.addEventListener('click', () => {
        if (selectedNode) {
          try {
            const properties = JSON.parse(nodeDataTextArea.value);
            selectedNode.setAttribute('data-properties', JSON.stringify(properties));
            propertyEditor.style.display = 'none';
            selectedNode = null;
          } catch (e) {
            alert('Invalid JSON format');
          }
        }
      });

      let startConnector = null;

      canvas.addEventListener('click', (e) => {
        if (e.target.classList.contains('connector')) {
          if (startConnector) {
            const endConnector = e.target;
            if (startConnector !== endConnector) {
              createLink(startConnector, endConnector);
              startConnector = null;
            }
          } else {
            startConnector = e.target;
          }
        } else if (e.target.tagName === 'path') {
          selectLink(e.target);
        }
      });

      canvas.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        if (e.target.tagName === 'path') {
          deleteLink(e.target);
        }
      });

      function createLink(startConnector, endConnector) {
        const path = document.createElementNS(svgNS, 'path');
        path.setAttribute('stroke', '#000');
        path.setAttribute('fill', 'none');
        path.dataset.startNodeId = startConnector.parentElement.dataset.id;
        path.dataset.startPosition = startConnector.dataset.position;
        path.dataset.endNodeId = endConnector.parentElement.dataset.id;
        path.dataset.endPosition = endConnector.dataset.position;
        canvas.appendChild(path);
        links.push(path);
        updateLinks();
      }

      function getConnectorCoords(connector) {
        const transform = connector.parentElement.getAttribute('transform');
        const translate = transform.match(/translate\(([^,]+),\s*([^\)]+)\)/);
        if (translate) {
          const parentX = parseFloat(translate[1]);
          const parentY = parseFloat(translate[2]);
          const cx = parseFloat(connector.getAttribute('cx'));
          const cy = parseFloat(connector.getAttribute('cy'));
          return { x: parentX + cx, y: parentY + cy };
        }
        return { x: 0, y: 0 };
      }

      function updateLinks() {
        links.forEach(link => {
          const startNode = document.querySelector(`[data-id="${link.dataset.startNodeId}"]`);
          const endNode = document.querySelector(`[data-id="${link.dataset.endNodeId}"]`);

          if (startNode && endNode) {
            const startConnector = startNode.querySelector(`.connector[data-position="${link.dataset.startPosition}"]`);
            const endConnector = endNode.querySelector(`.connector[data-position="${link.dataset.endPosition}"]`);
            const startCoords = getConnectorCoords(startConnector);
            const endCoords = getConnectorCoords(endConnector);

            // 使用折线路径连接节点
            const midX = (startCoords.x + endCoords.x) / 2;
            const midY = (startCoords.y + endCoords.y) / 2;
            const pathData = `M${startCoords.x},${startCoords.y} L${midX},${startCoords.y} L${midX},${endCoords.y} L${endCoords.x},${endCoords.y}`;
            link.setAttribute('d', pathData);
          }
        });
      }

      function selectLink(link) {
        if (selectedLink) {
          selectedLink.classList.remove('selected');
        }
        selectedLink = link;
        selectedLink.classList.add('selected');
      }

      function deleteLink(link) {
        if (link === selectedLink) {
          canvas.removeChild(link);
          const index = links.indexOf(link);
          if (index !== -1) {
            links.splice(index, 1);
          }
          selectedLink = null;
        }
      }
    });
  </script>
</body>
</html>
